# USAGE
# python detect_drowsiness.py --shape-predictor shape_predictor_68_face_landmarks.dat
# python detect_drowsiness.py --shape-predictor shape_predictor_68_face_landmarks.dat --alarm alarm.wav

# 导入必要的包文件
from scipy.spatial import distance as dist
from imutils.video import VideoStream
# 导入了face_utils模块，其中包含了用于处理人脸特征的工具函数和常量
from imutils import face_utils
from threading import Thread
import numpy as np
import playsound
import argparse
import imutils
import time
import dlib
import cv2

# 人脸关键点模型的位置
shape_predictor_path = "R:/Tools/pyworkplace/shape_predictor_68_face_landmarks.dat"
# 获取报警音频的位置
alarm_path = "R:/Tools/pyworkplace/face-rec/testProject/audio/didi.wav"


def eye_aspect_ratio(eye):
	# 计算垂直眼部特征的两者的欧氏距离
	# 坐标(x ,y)
	A = dist.euclidean(eye[1], eye[5])
	B = dist.euclidean(eye[2], eye[4])

	# 计算水平眼部特征的两者的欧氏距离
	C = dist.euclidean(eye[0], eye[3])

	# 计算眼睛长宽比并返回
	ear = (A + B) / (2.0 * C)
	return ear


def mouth_aspect_ratio(mouth):
	A = dist.euclidean(mouth[13], mouth[19])
	B = dist.euclidean(mouth[15], mouth[17])

	C = dist.euclidean(mouth[0], mouth[6])

	mouth_are = (A + B) / (2.0 * C)

	return mouth_are


# 构造命令行参数解析并解析参数
# ap = argparse.ArgumentParser()
# ap.add_argument("-p", "--shape-predictor", required=True,
# 	help="path to facial landmark predictor")
# ap.add_argument("-a", "--alarm", type=str, default="",
# 	help="path alarm .WAV file")
# ap.add_argument("-w", "--webcam", type=int, default=0,
# 	help="index of webcam on system")
# args = vars(ap.parse_args())

# 定义两个常数
# 一个用于指示眨眼的眼睛纵横比
EYE_AR_THRESH = 0.25
# 另一个用于表示眼睛必须低于阈值才能触发警报的连续帧数
EYE_AR_CONSEC_FRAMES = 20

# 初始化帧数计数器，并用一个布尔变量表示警报的运行状态
EYE_COUNTER = 0

MOUTH_AR_THRESH = 0.3
MOUTH_AR_CONSEC_FRAMES = 5
MOUTH_COUNTER = 0

# 记录过去一段时间内张嘴的行为次数，如果在一定时间内张嘴的次数为大于N， 则判定为打呵欠疲劳
behavior_times = []
# 嘴巴是否为闭合状态，当嘴巴闭上了，才算作一次有效张嘴
MOUTH_CLOSE = True

# 用于删除超过一分钟的行为记录
def update_behavior_times():
	current_time = time.time()
	# 删除超过一分钟的旧时间戳
	behavior_times[:] = [t for t in behavior_times if current_time - t <= 60]


def add_behavior():
	update_behavior_times()
	count = len(behavior_times)
	if count >= 3:        
		# 如果规定时间内行为超过3次，则删除离现在时间最久的一次记录
		oldest_time = min(behavior_times)
		behavior_times.remove(oldest_time)
		behavior_times.append(time.time())
	else:
		behavior_times.append(time.time())


ALARM_ON = False
sound_exit = True  # 主线程结束时，结束当前播放声音线程


def sound_alarm(path):
	# 播放报警声音
	while sound_exit:
		if ALARM_ON:
			playsound.playsound(path)


# 如果报警文件可用，则启动报警器
if alarm_path != "":
	t = Thread(target=sound_alarm, args=(alarm_path,))
	# t.deamon = True
	t.start()

# initialize dlib's face detector (HOG-based) and then create
# the facial landmark predictor
# 初始化dlib的人脸检测器（基于HOG），然后创建人脸地标预测器
print("[INFO] loading facial landmark predictor...")
detector = dlib.get_frontal_face_detector()
# predictor = dlib.shape_predictor(args["shape_predictor"])
predictor = dlib.shape_predictor(shape_predictor_path)

# 从face_utils模块中的FACIAL_LANDMARKS_IDXS字典中获取了左眼的索引范围。
# 这个范围通常是一个包含起始索引和结束索引的元组，用于提取出左眼的特征点
(lStart, lEnd) = face_utils.FACIAL_LANDMARKS_IDXS["left_eye"]
(rStart, rEnd) = face_utils.FACIAL_LANDMARKS_IDXS["right_eye"]

(mouth_start, mouth_end) = (48, 60)
(inner_mouth_start, inner_mouth_end) = (60, 68)

# vs = VideoStream(0).start()为获取摄像头并执行打开操作
print("[INFO] starting video stream thread...")
# vs = VideoStream(src=args["webcam"]).start()
vs = VideoStream(0).start()
time.sleep(1.0)

# 循环读取视频中的每一帧
while True:
	# 从vs对象中读取一帧图像，resize，转换成灰度通道，存储的frame中
	frame = vs.read()
	frame = imutils.resize(frame, width=450)
	gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

	# 检测灰度帧中的人脸
	rects = detector(gray, 0)

	# 遍历检测到的人脸
	for rect in rects:
		# 确定面部特征和面部区域, 然后将面部标志坐标（x, y）转化为 numpy 数组
		shape = predictor(gray, rect)
		shape = face_utils.shape_to_np(shape)

		# 提取出左右眼的坐标, 然后用坐标计算两个眼睛的纵横比
		leftEye = shape[lStart:lEnd]
		rightEye = shape[rStart:rEnd]
		leftEAR = eye_aspect_ratio(leftEye)
		rightEAR = eye_aspect_ratio(rightEye)

		# 嘴巴外围，凸包可视化
		# mouth = shape[mouth_start:inner_mouth_end]
		# mouthHull = cv2.convexHull(mouth)
		# cv2.drawContours(frame, [mouthHull], -1, (0, 255, 0), 1)
		# 嘴巴内侧，凸包可视化
		inner_mouth = shape[inner_mouth_start:inner_mouth_end]
		mouth = shape[mouth_start:inner_mouth_end]
		inner_mouthHull = cv2.convexHull(inner_mouth)
		cv2.drawContours(frame, [inner_mouthHull], -1, (0, 255, 0), 1)
		mouthEAR = mouth_aspect_ratio(mouth)

		# 两个眼睛的纵横比平均值
		ear = (leftEAR + rightEAR) / 2.0

		# 计算左眼和右眼的凸包，然后将每个眼睛可视化
		leftEyeHull = cv2.convexHull(leftEye)
		rightEyeHull = cv2.convexHull(rightEye)
		cv2.drawContours(frame, [leftEyeHull], -1, (0, 255, 0), 1)
		cv2.drawContours(frame, [rightEyeHull], -1, (0, 255, 0), 1)

		# 检查眼睛纵横比是否低于眨眼阈值，如果是，则增加眨眼帧计数器
		if ear < EYE_AR_THRESH:
			EYE_COUNTER += 1

			# 如果眼睛闭合帧数超过阈值，则触发警报
			if EYE_COUNTER >= EYE_AR_CONSEC_FRAMES:
				# 如果警报是关闭的，则打开报警器
				if not ALARM_ON:
					ALARM_ON = True

				# 在屏幕上显示警报消息
				print("*****[ERROR]: FATIGUE ALERT!*****")
				cv2.putText(frame, "DROWSINESS ALERT!", (10, 30),
							cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)

		# 如果眼睛纵横比高于阈值，则重置帧计数器和报警器
		else:
			EYE_COUNTER = 0
			ALARM_ON = False

		# 同理，检查嘴巴的纵横比是否高于张嘴阈值，如果是，则增加张嘴帧计数器
		if mouthEAR > MOUTH_AR_THRESH:
			MOUTH_COUNTER += 1



			# 如果嘴巴张开的帧数超过阈值，并且计数器大于2，则触发警报
			if MOUTH_COUNTER >= MOUTH_AR_CONSEC_FRAMES:
				# 增加计数器
				if MOUTH_CLOSE:
					add_behavior()
					MOUTH_CLOSE = False

				# 如果警报是关闭的，则打开报警器
				if not ALARM_ON and len(behavior_times) > 2:
					ALARM_ON = True

				# 在屏幕上显示警报消息
				print("*****[ERROR]: FATIGUE ALERT!*****")
				cv2.putText(frame, "DROWSINESS ALERT!", (10, 60),
							cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)

		# 如果嘴巴纵横比低于阈值，则重置帧计数器和报警器
		else:
			MOUTH_COUNTER = 0
			ALARM_ON = False
			MOUTH_CLOSE = True
		# 更新行为时间
		update_behavior_times()

		# 在帧上绘制计算出的眼睛宽高比，以帮助调试和设置正确的眼睛宽比阈值和帧计数器
		# 当处于安全范围时，文字显示为绿色，否则为红色
		if EYE_AR_THRESH > ear:
			cv2.putText(frame, "EAR: {:.2f}".format(ear), (300, 30),
						cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
		else:
			cv2.putText(frame, "EAR: {:.2f}".format(ear), (300, 30),
						cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
		if mouthEAR < MOUTH_AR_THRESH:
			cv2.putText(frame, "MOUTH: {:.2f}, COUNT:{}".format(mouthEAR, 1), (300, 60),
						cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
		else:
			cv2.putText(frame, "MOUTH: {:.2f}, COUNT:{}".format(mouthEAR, 1), (300, 60),
						cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
		if len(behavior_times) <= 2:
			cv2.putText(frame, "COUNT: {}".format(len(behavior_times)), (365, 80),
						cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
		else:
			cv2.putText(frame, "COUNT: {}".format(len(behavior_times)), (365, 80),
						cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)

	# show the frame
	cv2.imshow("Frame", frame)
	if cv2.waitKey(1) & 0xFF == ord('q'):
		sound_exit = False
		break

# do a bit of cleanup
cv2.destroyAllWindows()
vs.stop()
time.sleep(1.0)
